/**
 * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Tests for the useRegisterShortcut hook.
 */

import React, { act, ReactElement } from "react"

import * as hotkeysModule from "hotkeys-js"
import { Mock, vi } from "vitest"

import { render } from "~lib/test_util"
import * as Utils from "~lib/util/utils"

import {
  formatShortcutForDisplay,
  useRegisterShortcut,
} from "./useRegisterShortcut"

vi.mock("~lib/util/utils", async () => {
  const actual = await vi.importActual<typeof Utils>("~lib/util/utils")
  return {
    ...actual,
    isFromMac: vi.fn(),
  }
})

vi.mock("hotkeys-js", () => {
  const handlers = new Map<
    string,
    (keyboardEvent: KeyboardEvent, handler: unknown) => void
  >()

  const hotkeysMock = (
    keys: string,
    callback: (keyboardEvent: KeyboardEvent, handler: unknown) => void
  ): void => {
    handlers.set(keys, callback)
  }

  hotkeysMock.unbind = (
    keys: string,
    callback: (keyboardEvent: KeyboardEvent, handler: unknown) => void
  ): void => {
    const existing = handlers.get(keys)
    if (existing === callback) {
      handlers.delete(keys)
    }
  }

  let filterFn: (event: KeyboardEvent) => boolean = () => true
  Object.defineProperty(hotkeysMock, "filter", {
    get: () => filterFn,
    set: (value: (event: KeyboardEvent) => boolean) => {
      filterFn = value
    },
  })

  return {
    __esModule: true,
    default: hotkeysMock,
    __handlers: handlers,
    HotkeysEvent: class HotkeysEvent {},
  }
})

interface TestComponentProps {
  shortcut?: string | null
  disabled?: boolean
  onActivate: () => void
}

const hotkeysWithHandlers = hotkeysModule as typeof hotkeysModule & {
  __handlers: Map<
    string,
    (keyboardEvent: KeyboardEvent, handler: unknown) => void
  >
}

const TestComponent = ({
  shortcut,
  disabled,
  onActivate,
}: TestComponentProps): ReactElement => {
  useRegisterShortcut({ shortcut, disabled, onActivate })
  return <div>Shortcut test</div>
}

const createKeyboardEvent = (
  overrides: Partial<KeyboardEvent> = {}
): KeyboardEvent =>
  ({
    key: "k",
    preventDefault: vi.fn(),
    ctrlKey: false,
    metaKey: false,
    altKey: false,
    shiftKey: false,
    target: document.body,
    ...overrides,
  }) as unknown as KeyboardEvent

describe("useRegisterShortcut", () => {
  afterEach(() => {
    vi.clearAllMocks()
    hotkeysWithHandlers.__handlers.clear()
  })

  it("registers shortcut handlers", () => {
    const shortcut = "ctrl+k"
    const onActivate = vi.fn()

    render(
      <TestComponent
        shortcut={shortcut}
        disabled={false}
        onActivate={onActivate}
      />
    )

    const handler = hotkeysWithHandlers.__handlers.get("ctrl+k")
    expect(handler).toBeDefined()

    act(() => {
      handler?.(createKeyboardEvent({ ctrlKey: true }), {})
    })

    expect(onActivate).toHaveBeenCalledTimes(1)
  })

  it("does not trigger when shortcut is disabled", () => {
    const shortcut = "ctrl+k"
    const onActivate = vi.fn()

    render(
      <TestComponent
        shortcut={shortcut}
        disabled={true}
        onActivate={onActivate}
      />
    )

    const handler = hotkeysWithHandlers.__handlers.get("ctrl+k")
    expect(handler).toBeUndefined()
  })

  it("prevents activation when typing in text inputs without modifiers", () => {
    const shortcut = "n"
    const onActivate = vi.fn()

    render(
      <TestComponent
        shortcut={shortcut}
        disabled={false}
        onActivate={onActivate}
      />
    )

    const input = document.createElement("input")
    const handler = hotkeysWithHandlers.__handlers.get("n")
    expect(handler).toBeDefined()

    act(() => {
      handler?.(createKeyboardEvent({ target: input }), {})
    })

    expect(onActivate).not.toHaveBeenCalled()
  })

  it("prevents navigation shortcuts from firing in text inputs without modifiers", () => {
    const shortcut = "left"
    const onActivate = vi.fn()

    render(
      <TestComponent
        shortcut={shortcut}
        disabled={false}
        onActivate={onActivate}
      />
    )

    const input = document.createElement("input")
    const handler = hotkeysWithHandlers.__handlers.get("left")
    expect(handler).toBeDefined()

    act(() => {
      handler?.(createKeyboardEvent({ target: input, key: "ArrowLeft" }), {})
    })

    expect(onActivate).not.toHaveBeenCalled()
  })

  it("allows navigation shortcuts with system modifiers in text inputs", () => {
    const shortcut = "ctrl+left"
    const onActivate = vi.fn()

    render(
      <TestComponent
        shortcut={shortcut}
        disabled={false}
        onActivate={onActivate}
      />
    )

    const input = document.createElement("input")
    const handler = hotkeysWithHandlers.__handlers.get("ctrl+left")
    expect(handler).toBeDefined()

    act(() => {
      handler?.(
        createKeyboardEvent({
          target: input,
          ctrlKey: true,
          key: "ArrowLeft",
        }),
        {}
      )
    })

    expect(onActivate).toHaveBeenCalled()
  })

  it("registers command shortcut as Cmd on Mac", () => {
    ;(Utils.isFromMac as Mock).mockReturnValue(true)
    const shortcut = "cmd+n"
    const onActivate = vi.fn()

    render(
      <TestComponent
        shortcut={shortcut}
        disabled={false}
        onActivate={onActivate}
      />
    )

    const commandHandler = hotkeysWithHandlers.__handlers.get("command+n")
    const ctrlHandler = hotkeysWithHandlers.__handlers.get("ctrl+n")

    expect(commandHandler).toBeDefined()
    expect(ctrlHandler).toBeUndefined()
  })

  it("registers command shortcut as Ctrl on non-Mac", () => {
    ;(Utils.isFromMac as Mock).mockReturnValue(false)
    const shortcut = "cmd+n"
    const onActivate = vi.fn()

    render(
      <TestComponent
        shortcut={shortcut}
        disabled={false}
        onActivate={onActivate}
      />
    )

    const commandHandler = hotkeysWithHandlers.__handlers.get("command+n")
    const ctrlHandler = hotkeysWithHandlers.__handlers.get("ctrl+n")

    expect(commandHandler).toBeUndefined()
    expect(ctrlHandler).toBeDefined()
  })
})

describe("formatShortcutForDisplay", () => {
  it("renders Option for alt modifiers on macOS", () => {
    expect(formatShortcutForDisplay("Alt+S", { isMac: true })).toBe("⌥ + S")
  })

  it("renders correct modifier for mod on macOS", () => {
    expect(formatShortcutForDisplay("Mod+S", { isMac: true })).toBe("⌘ + S")
  })

  it("renders correct modifier for mod on non-mac platforms", () => {
    expect(formatShortcutForDisplay("Mod+S", { isMac: false })).toBe(
      "Ctrl + S"
    )
  })

  it("renders Alt modifiers on non-mac platforms", () => {
    expect(formatShortcutForDisplay("Alt+S", { isMac: false })).toBe("Alt + S")
  })

  it("ignores casing and extra whitespace", () => {
    expect(formatShortcutForDisplay("  cTrL  +   alt +   n  ")).toBe(
      "Ctrl + Alt + N"
    )
  })
})
